package org.hertsig.mci;

import static com.google.common.base.Preconditions.checkState;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.hertsig.entities.Card;
import org.hertsig.entities.ManaCost;
import org.hertsig.entities.Rarity;
import org.hertsig.entities.Type;
import org.hertsig.entities.Types;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ExecutionError;
import com.google.common.util.concurrent.UncheckedExecutionException;

@Slf4j
public class MagicCardsInfo {
	private static final String BASE_URL = "http://magiccards.info/";
	
	private static final char NOT_A_QUOTE = ''; // I'm still not sure what character this is, but MCM puts it between 'Urza' and 's'
	private static final char ACTUAL_QUOTE = '\'';
	
	private static final Pattern EDITION_RARITY_PATTERN = Pattern.compile("(.+?)\\((.+?)\\)+");
	
	public static final ImmutableSet<String> UNSUPPORTED_SETS = ImmutableSet.of("uh", "unh", "ug", "ugl", "uhaa", "hho", "uqc", "arena", "ppr", "van");
	
	@Getter(lazy=true) private static final Types types = initTypes();
	@Getter(lazy=true) private static final ImmutableBiMap<String, String> sets = initSets();
	
	private static final Cache<String, List<Card>> cardCache = CacheBuilder.newBuilder().build();
	
	/**
	 * Executes the given magiccards.info query. Returns a {@link Card} for each distinct card, from any one edition it was printed in.
	 * @see <a href=http://magiccards.info/syntax.html>MagicCards.info query syntax</a>
	 */
	@SneakyThrows(IOException.class)
	public static List<Card> search(String query) {
		String url = BASE_URL + "query?q=" + URLEncoder.encode(query, "UTF-8") + "&v=olist&s=issue";
		Document document = Jsoup.connect(url).get();
		List<Card> result = Lists.newArrayList();
		Elements rows = document.select("tr.even, tr.odd");
		if (rows.isEmpty()) {
			return Lists.newArrayList(parseSingle(document).get(0));
		}
		
		for (Element row : rows) {
			Card card = readCard(row.select("td").iterator());
			if (card != null) {
				result.add(card);
			}
		}
		return result;
	}
	
	/**
	 * Queries the card with the given name. Returns a {@link Card} for each edition it was printed in.
	 */
	@SneakyThrows(IOException.class)
	public static List<Card> getSingle(String name) {
		try {
			return cardCache.get(name, () -> parseSingle(Jsoup.connect(BASE_URL + "query?q=" + URLEncoder.encode("!" + name, "UTF-8")).get()));
		}
		catch (UncheckedExecutionException | ExecutionException | ExecutionError e) {
			Throwables.propagateIfPossible(e.getCause(), IOException.class);
			throw Throwables.propagate(e.getCause());
		}
	}
	
	/**
	 * For queries with only one result, a different page is returned
	 */
	private static List<Card> parseSingle(Document document) {
		Elements select = document.select("table");
		if (select.size() < 3) {
			return ImmutableList.of();
		}
		
		Iterator<Element> nodes = select.get(3).select("td").get(1).children().iterator();
		String name = nodes.next().select("a").text();
		String[] typeAndCost = nodes.next().text().split(",");
		String typeAndPT = typeAndCost[0].trim();
		String cost = typeAndCost.length == 1 ? "" : typeAndCost[1].substring(0, typeAndCost[1].indexOf(' ')).trim();
		String rules = nodes.next().text();
		String flavor = nodes.next().text();
		String artist = nodes.next().text();
		
		List<Card> result = Lists.newArrayList();
		String editions = document.select("td:contains(Editions)").text();
		editions = editions.substring(editions.indexOf("Editions:") + 10, editions.indexOf("Languages:") - 1).trim();
		Matcher matcher = EDITION_RARITY_PATTERN.matcher(editions);
		while (matcher.find()) {
			String edition = matcher.group(1).trim();
			String rarity = matcher.group(2).trim();
			result.add(new Card(name, Type.parse(typeAndPT), ManaCost.parse(cost), Rarity.getByName(rarity), getSetCode(edition), artist, rules, flavor));
		}
		
		return result;
	}
	
	private static String getSetCode(String setName) {
		String code = getSets().inverse().get(setName.trim());
		return Preconditions.checkNotNull(code, "No code found for set " + setName);
	}
	
	private static Card readCard(Iterator<Element> nodes) {
		String name = nodes.next().select("a").text();
		String typeAndPT = nodes.next().text();
		String cost = nodes.next().text();
		String rarity = nodes.next().text();
		String artist = nodes.next().text();
		String edition = nodes.next().text();
		if (UNSUPPORTED_SETS.contains(getSets().inverse().get(edition))) {
			log.trace("Card {} is from unsupported set {}, skipping", name, edition);
			return null;
		}
		return new Card(name, Type.parse(typeAndPT), ManaCost.parse(cost), Rarity.getByName(rarity), getSets().inverse().get(edition), artist);
	}
	
	@SneakyThrows(IOException.class)
	private static Types initTypes() {
		Document document = Jsoup.connect(BASE_URL + "rule/205-type-line.html").get();
		ImmutableSet<String> artifact = findTypes("artifact", document, "g");
		ImmutableSet<String> enchantment = findTypes("enchantment", document, "h");
		ImmutableSet<String> land = findTypes("land", document, "i");
		ImmutableSet<String> planeswalker = findTypes("planeswalker", document, "j", "Dack");
		ImmutableSet<String> creature = findTypes("creature", document, "m", "Lamia");
		ImmutableSet<String> plane = findTypes("planar", document, "n");
		
		checkState(document.select("li[id=rule-205-3-k]").first().select("p").first().ownText().endsWith("The spell types are Arcane and Trap."), "They changed the spell types!");
		ImmutableSet<String> spell = ImmutableSet.of("Arcane", "Trap");
		
		return new Types(creature, spell, artifact, enchantment, planeswalker, land, plane);
	}
	
	private static ImmutableSet<String> findTypes(String type, Document document, String ruleIndex, String... additionalTypes) {
		String text = document.select("li[id=rule-205-3-" + ruleIndex + "]").first().select("p").first().ownText();
		String key = "The " + type + " types are ";
		
		ImmutableSet.Builder<String> builder = ImmutableSet.builder();
		String[] types = text.substring(text.indexOf(key) + key.length()).split(",\\s");
		for (String subtype : types) {
			int begin = subtype.startsWith("and ") ? 4 : 0;
			int end = subtype.indexOf(' ', begin + 1);
			if (end == -1) {
				end = subtype.indexOf('.');
			}
			if (end == -1) {
				end = subtype.length();
			}
			builder.add(subtype.replace(NOT_A_QUOTE, ACTUAL_QUOTE).substring(begin, end));
		}
		builder.add(additionalTypes);
		return builder.build();
	}
	
	@SneakyThrows(IOException.class)
	private static ImmutableBiMap<String, String> initSets() {
		Document document = Jsoup.connect(BASE_URL + "sitemap.html").get();
		ImmutableBiMap.Builder<String, String> sets = ImmutableBiMap.builder();
		for (Element blockNode : document.select("h2 ~ table").first().select("a")) {
			String setCode = blockNode.siblingElements().first().ownText();
			sets.put(setCode, blockNode.ownText());
		}
		return sets.build();
	}
}
